package com.google.code.yargon.level;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;

import org.apache.log4j.Logger;

import com.google.code.yargon.Yargon;
import com.google.code.yargon.engine.*;
import com.google.code.yargon.util.*;

import static com.google.code.yargon.util.Constants.*;

/**
 * Level generator
 * 
 * For now all pieces can generate themselves (see Room.java) and this class is supposed to
 * fill the map with different things.
 *
 * @author Mika Myllynen
 *
 */

@SuppressWarnings("serial")
public class LevelManager extends LinkedList<LevelArea> {

	private int roomMinW = 6;
	private int roomMinH = 6;
	private int roomMaxW = 24;
	private int roomMaxH = 24;
	
	private int roomBuffer = 5;
	private int borderBuffer = 5;
	
	private int maxRooms = 20;

	// Logger
	private Logger log = LogUtil.getInstance();
	
	// Singleton instance
	private static LevelManager instance = null;
	
	/**
	 * Private constructor - singleton class
	 */
	private LevelManager() {
	}
	
	/**
	 * Get the LevelGenerator singleton instance
	 * 
	 * @return
	 */
	public static LevelManager getInstance() {
		if(instance == null) {
			instance = new LevelManager();
		}
		return instance;
	}
	
	/** 
	 * Set room minimum width
	 * 
	 * @param roomMinW
	 */
	public void setRoomMinW(int roomMinW) {
		this.roomMinW = roomMinW;
	}

	/**
	 * Set room minimum height
	 * 
	 * @param roomMinH
	 */
	public void setRoomMinH(int roomMinH) {
		this.roomMinH = roomMinH;
	}

	/**
	 * Set room maximum width
	 * 
	 * @param roomMaxW
	 */
	public void setRoomMaxW(int roomMaxW) {
		this.roomMaxW = roomMaxW;
	}

	/** Set room maximum height
	 * 
	 * @param roomMaxH
	 */
	public void setRoomMaxH(int roomMaxH) {
		this.roomMaxH = roomMaxH;
	}

	/**
	 * Set the buffer zone between rooms (minimum free area between two rooms)
	 * 
	 * @param roomBuffer
	 */
	public void setRoomBuffer(int roomBuffer) {
		this.roomBuffer = roomBuffer;
	}

	/**
	 * Set the buffer zone between room and level boundaries (minimum free area between room and level boundaries)
	 * @param borderBuffer
	 */
	public void setBorderBuffer(int borderBuffer) {
		this.borderBuffer = borderBuffer;
	}

	/**
	 * Set the maximum amount of rooms to generate. Note: This may not be reached if the map is full before this limit.
	 * 
	 * @param maxRooms
	 */
	public void setMaxRooms(int maxRooms) {
		this.maxRooms = maxRooms;
	}

	/**
	 * Generate a level
	 * 
	 * @param level - level object to be filled with random rooms etc
	 */
	public void generate(int levelNo) {
			
		int genRooms = 0;
		long st = YargonUtil.getTime();
		LevelArea level = get(levelNo);
		
		for(int nRooms = 0; nRooms < maxRooms; nRooms++) { 
		
			// Room location and dimensions - randomized
			int rw = 0;
			int rh = 0;
			int rx = 0;
			int ry = 0;
			
			// Loop control
			int tryCount = 0;
			int maxTries = 100;
			
			// No worries, loop exited by breaks when conditions are met
			while(true) {
				tryCount++;
						
				rw = Cast.castValueBetween(roomMinW, roomMaxW);
				rh = Cast.castValueBetween(roomMinH, roomMaxH);
			
				// And placement
				rx = Cast.castValueBetween(borderBuffer, level.getWidth() - rw - 1 - borderBuffer);
				ry = Cast.castValueBetween(borderBuffer, level.getHeight() - rh - 1 - borderBuffer);
			
				// Check if room fits - including buffer zone between rooms
				Area tmp = level.subArea(rx - roomBuffer, 
										 ry - roomBuffer, 
										 rw + (roomBuffer * 2), 
										 rh + (roomBuffer * 2));
				
				// Give up if we've tried enough
				if(! level.isFree(tmp)) {
					if(tryCount == maxTries) {
						log.warn("Tried to fit a room "+maxTries+" times, did not find free location. Giving up.");
						break;
					}
					continue;
				} 
				
				// Room fits, generate it and put it on the map
				log.trace("Found location for a "+rw+"x"+rh+" room at "+rx+","+ry+". Took "+tryCount+" tries.");
				RoomArea room = new RoomArea(rw, rh);
				generateRoom(room);
					
				// Put the room in the map
				level.put(rx, ry, room);
					
				// If this is first room, put the player character in the middle of it
				if(nRooms == 0) {
					Location pcLoc = new Location(rx + (rw / 2), ry + (rh / 2), 0);
					Yargon.getInstance().getPlayerCharacter().setLocation(pcLoc);
					
					// FIXME: Testing only - add new creature to east from player
					AI.getInstance().add(pcLoc.toEast());
					
				}
				genRooms++;
				break;
			}
		}
		log.debug("Generated "+genRooms+" rooms out of maximum "+maxRooms+".");
		
		// Get a list of all doors on the level
		HashMap<Block, Integer> ad = level.list(DoorBlock.class);
		log.trace("Level has "+ad.size()+" doors.");
		
		//  Get a list of usable doors on the level (discard doors that do not have free block in their facing direction)
		HashMap<DoorBlock, Integer> ud = new HashMap<DoorBlock, Integer>();
		for(Map.Entry<Block, Integer> entry : ad.entrySet()) {
			DoorBlock db = (DoorBlock)entry.getKey();
			if(db.isConnectable()) {
				ud.put(db,  0);
			}
		}
		log.trace("Of which "+ud.size()+" are connectable.");
			
		// Iterate through remaining door and connect them
		int n = 0;
		for(Map.Entry<DoorBlock, Integer> entry : ud.entrySet()) {
					
			// Set source as current and destination random (not the source)
			int dn = n; 
			while(dn == n) {
				dn = Cast.castValueBetween(0, ud.size() - 1);
			}
			DoorBlock srcDoor = entry.getKey();
			DoorBlock destDoor = (DoorBlock)ud.keySet().toArray()[dn];
			
			// Route end points, adjust according to the door facings (route starts and ends on the blocks the door faces)
			Location srcLoc = srcDoor.getLocation().toDirection(srcDoor.getDirection());
			Location destLoc = destDoor.getLocation().toDirection(destDoor.getDirection());
			
			Route r = new Route(srcLoc, destLoc);
			RouteFinder rf = new RouteFinder(r, level);
			int connectionType = rf.solveHomingPath();
			if(r.isSolved()) {
				if(connectionType == 1) {
					log.trace("Door "+srcDoor.toString()+" connected to door "+destDoor.toString());
				} else {
					log.trace("Door "+srcDoor.toString()+" connected to tunnel at "+r.getLast().toString());
				}
				for(Location l : r.getPoints()) {
					level.put(l, new TunnelBlock(0));
				}
			} else {
				log.warn("Door "+srcDoor.toString()+" could not be connected! Target was "+destDoor.toString());
				/*
				for(Location l : r.getWayPoints()) {
					level.put(l, new FloorBlock());
				}
				break; */
			}
			n++;		
		} 
		
		log.debug("Generating level took "+(YargonUtil.getTime() - st)+" ms");
	} 
			
	/**
	 * Generate contents for a room
	 * 
	 */
	private void generateRoom(RoomArea room) {	
		
		int max;
		int n;
		
		// Start with maximum sized room
		roomMaximize(room);
		
		// Reshape the room n times if it is larger than 4x4
		if(room.getWidth() > 4 && room.getHeight() > 4) {
			max = (int)(room.getWidth() + room.getHeight() / 4); 	// Max reshapes depend on room size
			n = Cast.castValueBetween(0, max); 
			for(int i = 0; i < n; i++) {
				roomReshape(room);
			}
		}
		
		// Add doors to the room

		n = Cast.castValueBetween(1, 4);
		for(int i = 0; i < n; i++) {
			roomFitDoor(room);
		} 
	}

	/**
	 * Generate a max sized room in the given Room object, ie. just put walls on the boundaries
	 * 
	 * @param room
	 * @return
	 */
	private void roomMaximize(RoomArea room) {
		for(int y = 0; y < room.getHeight(); y++) {
			for(int x = 0; x < room.getWidth(); x++) {			
				if(x == 0 || y == 0 || x == room.getWidth() - 1 || y == room.getHeight() - 1) {
					room.put(x, y, new WallBlock()); 
				} else {
					room.put(x, y, new FloorBlock());
				}
			}
		}
	}
	
	/**
	 * Reshape a room<br><br>
	 * Adds a solid section in the room, carving out insides and reshaping the original walls<br><br>
	 * 
	 * NOTE: This method may split the room in smaller side rooms (if carved sections intersect in the corners).
	 * These split sections may or may have not doors depending where random placement puts them. We call this a feature and secret areas which may be 
	 * for example dug into later :)
	 * 
	 * @param room
	 */
	private void roomReshape(RoomArea room) {
		
		// Placement and dimensions for reshaped section 
		int x = 0;
		int y = 0;
		int w = 0;
		int h = 0;
		int maxW = 2;
		int maxH = 2;
		
		// Max dimensions are 1/3 of the room dimensions if room is > 7, otherwise no more than 2
		if(room.getHeight() > 7) {
			maxH = (int)(room.getHeight() / 3);
		}
		if(room.getWidth() > 7) {
			maxW = (int)(room.getWidth() / 3);
		}
		
		// Randomize to which side should be reshaped and determine (random) dimensions
		int s =  Cast.castValueBetween(0, 3);
		switch(s) {
					
		// North
		case 0:
			x = Cast.castValueBetween(0, room.getWidth() - 1); 			// SW-corner anywhere on x-axis
			w = Cast.castValueBetween(1, room.getWidth() - x); 			// Width 1..anything that fits in the remaining axis 	
			h = Cast.castValueBetween(2, maxH); 							// Height 2..max solid height
			y = room.getHeight() - h;									// SW-corner at (top of the room) - (determined height) on y-axis
			break;
	
		// East
		case 1:
			y = Cast.castValueBetween(0, room.getHeight() - 1);			// SW corner anywhere on y-axis
			w = Cast.castValueBetween(2, maxW); 							// Width 2..max solid width 	
			h = Cast.castValueBetween(1, room.getHeight() - y);			// Height 1..anything that fits in the remaining axis
			x = room.getWidth() - w; 					 				// SW-corner at (left wall) - (determined width) on x-axis
			break;
		
		// South
		case 2:
			y = 0;														// SW-corner at 0 y-axis
			x = Cast.castValueBetween(0, room.getWidth() - 1); 			// SW-corner anywhere on x-axis
			w = Cast.castValueBetween(1, room.getWidth() - x); 			// Width 1..anything that fits in the remaining axis 	
			h = Cast.castValueBetween(2, maxH); 							// Height 2..max solid height
			break;
		
		// West
		case 3:
			x = 0; 									 					// SW-corner at 0 on x-axis
			y = Cast.castValueBetween(0, room.getHeight() - 1);			// SW corner anywhere on y-axis
			w = Cast.castValueBetween(2, maxW); 							// Width 2..max solid width 	
			h = Cast.castValueBetween(1, room.getHeight() - y);			// Height 1..anything that fits in the remaining axis
			break;		
		} 
		
		// Fill the reshaped area with walls
		for(int fy = y; fy < y + h; fy++) {
			for(int fx = x; fx < x + w; fx++) {
				room.put(fx, fy, new WallBlock());
			}
		}	
		
		// And remove any invisible wall sections
		roomFinalizeWalls(room);
	}
	
	/**
	 * Finalize room walls, ie. Leave only those around that have at least one connecting floor block. 
	 * Otherwise the wall is invisible and can be safely removed.
	 * 
	 * @param room
	 */
	private void roomFinalizeWalls(RoomArea room) {
		
		// Check eats block
		for(int y = 0; y < room.getHeight(); y++) {
			for(int x = 0; x < room.getWidth(); x++) {
				
				int nFloor = 0;
				
				// If it is wall, check all surrounding blocks for a floor
				if(room.isWall(x, y)) {
					for(int iy = y - 1; iy < y + 2; iy++) {
						for(int ix = x - 1; ix < x + 2; ix++) {
							if(room.isFloor(ix, iy)) {
								nFloor++;
							}
						}						
					}
				
					// No connecting floors, the wall can be removed
					if(nFloor == 0) {
						room.put(x, y, new UndefinedBlock());
					}
				}
			}
		}	
	}
	
	private void roomFitDoor(RoomArea room) {
			
		// Brute force door fitting
		int l = 0;
		while(true) {
			l++;
			
			int dx = 0;
			int dy = 0;			
			int dSide = Cast.castValueBetween(0, 3);										// What side of the room the door should go
			boolean ok = false;
			Direction dDir = null;
			
			switch(dSide) {
			
			// North
			case 0:
				dx = Cast.castValueBetween(1, room.getWidth() - 2);							// Start on random point on x-axis avoiding corners
				for(int y = room.getHeight() - 1; y > room.getHeight() / 2; y--) {			// Start from top on y-axis and go down looking for the wall
					if(room.isWall(dx, y) && 												// Is this OK place for a door? Must be a wall block..
					   room.isWall(dx - 1, y) && room.isWall(dx + 1, y) &&					// .. and must have walls on both sides..
					   room.isOfType(dx, y - 1, FloorBlock.class, UndefinedBlock.class) &&	// .. and floor or yet to be defined blocks in front..
					   room.isOfType(dx, y + 1, FloorBlock.class, UndefinedBlock.class)) { 	// .. and back
						dy = y;
						dDir = new Direction(NORTH);
						ok = true;
						break;
					}
				}
				break;
				
			// East
			case 1:
				dy = Cast.castValueBetween(1, room.getHeight() - 2);						// Start on random point on y-axis avoiding corners
				for(int x = room.getWidth() - 1; x > room.getWidth() / 2; x--) {			// Start left on x-axis and go right looking for the wall					
					if(room.isWall(x, dy) && 												// Is this OK place for a door? Must be a wall block..
					   room.isWall(x, dy - 1) && room.isWall(x, dy + 1) &&					// .. and must have walls on both sides..
					   room.isOfType(x - 1, dy, FloorBlock.class, UndefinedBlock.class) &&	// .. and floor or yet to be defined blocks in front..
					   room.isOfType(x + 1, dy, FloorBlock.class, UndefinedBlock.class)) {	// .. and back	
						dx = x;
						dDir = new Direction(EAST);
						ok = true;
						break;
					}
				}
				break;
				
			// South
			case 2:
				dx = Cast.castValueBetween(1, room.getWidth() - 2);							// Start on random point on x-axis avoiding corners
				for(int y = 0; y < room.getHeight() / 2; y++) {								// Start from bottom on y-axis and go up looking for the wall
					if(room.isWall(dx, y) && 												// Is this OK place for a door? Must be a wall block..
					   room.isWall(dx - 1, y) && room.isWall(dx + 1, y) &&					// .. and must have walls on both sides..
					   room.isOfType(dx, y - 1, FloorBlock.class, UndefinedBlock.class) &&	// .. and floor or yet to be defined blocks in front..
					   room.isOfType(dx, y + 1, FloorBlock.class, UndefinedBlock.class)) {	// .. and back
						dy = y;
						dDir = new Direction(SOUTH);
						ok = true;
						break;
					}
				}
				break;
				
			// West
			case 3:
				dy = Cast.castValueBetween(1, room.getHeight() - 2);						// Start on random point on y-axis avoiding corners
				for(int x = 0; x < room.getWidth() / 2; x++) {								// Start right on x-axis and go left looking for the wall		
					if(room.isWall(x, dy) && 												// Is this OK place for a door? Must be a wall block..
					   room.isWall(x, dy - 1) && room.isWall(x, dy + 1) &&					// .. and must have walls on both sides..
					   room.isOfType(x - 1, dy, FloorBlock.class, UndefinedBlock.class) && 	// .. and floor or yet to be defined blocks in front..
					   room.isOfType(x + 1, dy, FloorBlock.class, UndefinedBlock.class)) {	// .. and back
						dx = x;
						dDir = new Direction(WEST);
						ok = true;
						break;
					}
				}
				break;
			}
			
			// The door location is ok
			if(ok) {	
				Location dLoc = new Location(dx, dy, 0);
				boolean open = false;
				boolean connectable = false;
				int durability = 100;												
				
				if(Cast.castValueBetween(1, 100) > 90) {							// 10% probability that the door is already open
					open = true;
				}
				if(Cast.castValueBetween(1, 100) > 95) {							// 5% probability that the door is already broken
					durability = 0;
				}
				if(room.isUndefined(dLoc.toDirection(dDir))) {						// If the block the door faces is free, then the door is connectable
					connectable = true;
				} 
				DoorBlock db = new DoorBlock(open, connectable, dDir);
				db.setDurability(durability);
				room.put(dx, dy, db);												// Put the door on the area
				
				log.trace("Found location for a door at ("+dx+","+dy+"). Took "+l+" tries.");
				break;
			}
			
			if(l > 100) {
				log.warn("Brute force door fitting failed to find a door location!");
				break;
			}
		}
	}
}
